Skip to content

fix(cli): detect overlapping review comment ranges - #9801

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
bluefateludi:codex/fix-review-range-overlap
Aug 31, 2026
Merged

fix(cli): detect overlapping review comment ranges#9801
wenshao merged 1 commit into
QwenLM:mainfrom
bluefateludi:codex/fix-review-range-overlap

Conversation

@bluefateludi

Copy link
Copy Markdown
Contributor

What this PR does

Preserves the start of multi-line review findings and classifies existing inline comments by same-file closed-interval intersection instead of comparing only their ending lines. Exact single-line behavior remains unchanged, and carried-finding re-post detection remains tied to the exact original anchor.

Why it's needed

/review presubmit could report noConflict when an existing comment sat inside a newly drafted multi-line finding. That allowed a duplicate finding to pass the deterministic overlap gate and forced reviewers to detect it manually.

Reviewer Test Plan

How to verify

  1. Run cd packages/cli && npx vitest run src/commands/review/presubmit.test.ts.
  2. Confirm all 109 tests pass.
  3. Confirm the regression cases classify a point inside a new range and two partially intersecting ranges as overlap, while disjoint ranges remain noConflict.
  4. Confirm the existing carried-id and re-post tests remain green, showing that range overlap does not broaden thread reuse.

Evidence (Before & After)

Before: the focused regression produced two failures because a comment at line 15 did not match a new finding spanning lines 12–18, and partially intersecting ranges were also reported as noConflict.

After: the complete presubmit test file passes 109/109, including point-inside-range, partial intersection, disjoint control, parsing, exact-line, and carried-id/re-post coverage. An independent test-engineer run passed the 19 parsing/range-focused tests and git diff --check.

Tested on

OS Status
🍏 macOS ⚠️
🪟 Windows
🐧 Linux ⚠️

Environment (optional)

Windows 11, Node.js 22.19.0, npm 10.x, Vitest 3.2.7. Repository-wide build and typecheck passed; focused Prettier and ESLint checks passed.

Risk & Scope

Linked Issues

Partially addresses #9219.

中文说明

本 PR 做了什么

保留多行 review finding 的起始行,并将既有行内评论的冲突判定从“仅比较结束行”改为“同一文件内闭区间相交”。单行行为保持不变,carried finding 的 re-post 识别仍严格绑定原始精确锚点。

为什么需要它

当既有评论位于新起草的多行 finding 范围内部时,/review presubmit 可能错误报告 noConflict。这会让重复 finding 穿过确定性 overlap gate,只能依赖 reviewer 手工发现。

Reviewer Test Plan

如何验证

  1. 运行 cd packages/cli && npx vitest run src/commands/review/presubmit.test.ts
  2. 确认 109 个测试全部通过。
  3. 确认回归用例将“点位于新区间内部”和“两个区间部分相交”归类为 overlap,同时不相交区间仍归类为 noConflict
  4. 确认既有 carried-id 与 re-post 测试保持通过,证明范围冲突不会放宽 thread 复用。

修改前后证据

修改前:定向回归产生两个失败;位于第 15 行的评论无法匹配第 12–18 行的新 finding,部分相交的区间也被报告为 noConflict

修改后:完整 presubmit 测试文件 109/109 通过,覆盖范围内点、部分相交、不相交对照、解析、精确单行以及 carried-id/re-post。独立 test-engineer 运行的 19 个解析/范围相关测试与 git diff --check 也通过。

测试平台

OS 状态
🍏 macOS ⚠️
🪟 Windows
🐧 Linux ⚠️

环境

Windows 11、Node.js 22.19.0、npm 10.x、Vitest 3.2.7。仓库级 build 与 typecheck 通过;定向 Prettier 与 ESLint 检查通过。

风险与范围

关联 Issue

部分解决 #9219,不自动关闭该跟踪 Issue。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Re-run (2026-08-31): gate results are unchanged at the same head. The owner call the previous pass deferred on has since landed — see the Stage 3 comment for the verdict.

Problem: observed bug, not theoretical. Linked #9219 documents a concrete occurrence (a manual /review presubmit run on PR 9204, commit 40ad8fd): a drafted multi-line finding (start_line: 2554, line: 2562) with an existing comment sitting at line 2558 inside that range was bucketed noConflict, because only the two line fields were compared for exact equality. The issue also lists two further buckets of misses (semantic duplicates, nearby-line proximity) that this PR explicitly leaves out — good scoping. Since the first pass, the subsystem owner measured real traffic: about one in five Qwen /review inline comments (97 of 455 across 8 recent PRs) are multi-line, so range-aware overlap matching fires on real review runs, not a contrived case.

Direction: aligned — this is the project's own review-tooling dedup gate, fixing the first bullet of an open, reproduced P2 bug. CHANGELOG has no direct reference (internal tooling), but the area is squarely relevant. Doesn't touch auth/sandbox/model-selection/telemetry or any public contract.

Size: not applicable — packages/cli/src/commands/review/ is not a core-module path. 39 production lines (+30/−9 in presubmit.ts), 67 test lines (+66/−1), two files.

Approach: focused and minimal — carry start_line through the findings-file parser and the API comment shape, then classify by same-file closed-interval intersection instead of exact-line equality, with the carried-id re-post exemption deliberately kept exact-anchor. The one substantive question from the first pass — whether the skill-side consumer contract (findings-file shape, report comment shape, Step 7 drop-rule wording) rides along here or lands as a follow-up — is now settled: the author offered the follow-up, the subsystem owner verified the seam gaps in depth, judged the change safe to merge as-is, and approved. The remaining scope stays tracked on #9219, which this PR deliberately does not close.

Risk: no elevated risk signals — neither changed file matches the revert-correlated high-risk paths.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

复查(2026-08-31): 同一 head 下门检结果不变。上一次运行转交维护者定夺的问题已有结论——结论见 Stage 3 评论。

问题: 是已观测到的 bug,不是理论性问题。关联的 #9219 记录了具体场景(对 PR 9204、commit 40ad8fd 的一次人工 /review presubmit 运行):起草的多行 finding(start_line: 2554line: 2562)范围内第 2558 行有一条既有评论,却因只精确比较两个 line 字段而被归入 noConflict。该 issue 还列了另外两类漏检(语义重复、邻近行),本 PR 明确不处理——范围划分合理。首轮之后,子系统维护者统计了真实流量:近期 8 个 PR 中约五分之一的 Qwen /review 行内评论(455 条中的 97 条)是多行的,因此区间感知的冲突判定作用于真实的 review 运行,而非构造场景。

方向: 对齐——这是项目自身 review 工具的去重门,修复一个已复现的 P2 bug 的第一条。CHANGELOG 无直接引用(内部工具),但该领域明显相关。不触及 auth/沙箱/模型选择/遥测或任何公共契约。

规模: 不适用——packages/cli/src/commands/review/ 不是核心模块路径。生产代码 39 行(presubmit.ts +30/−9),测试 67 行(+66/−1),共两个文件。

方案: 聚焦且最小——把 start_line 贯穿 findings 文件解析与 API 评论结构,用同文件闭区间相交替代精确行相等,carried-id re-post 豁免有意保持精确锚点。首轮提出的实质性问题——skill 侧消费契约(findings 文件形状、报告评论形状、Step 7 删除规则措辞)是随本 PR 一起还是作为后续跟进——现已敲定:作者提出作为跟进,子系统维护者深入验证接缝缺口后判定改动按现状可安全合入并已批准。剩余范围继续在 #9219 下跟踪,本 PR 有意不关闭该 issue。

风险: 无升级风险信号——两个改动文件均未命中与回滚相关的高风险路径。

进入代码审查。🔍

Qwen Code · qwen3.8-max

Reviewed at b8038ece96afd18bc0a1d5259eca3ff328e7cad9 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Code review

Re-read at b8038ece — the head is unchanged since the first pass, and this pass re-verified the diff and the consumer-contract state on main rather than trusting the prior write-up. My independent proposal for this problem is the same shape as what landed: carry the range on both ends, classify by same-file closed-interval intersection, keep the carried-id re-post exemption on the exact anchor.

The command-side change is correct and careful. The range math normalizes missing/reversed ends with Math.min/Math.max, and both ends default to the point anchor when start_line is absent — so single-line inputs behave bit-for-bit as before, which the large pre-existing overlap/repost suite pins. Range intersection is a strict superset of exact-line equality, so nothing that used to land in overlap escapes it, and carriedIdsByLocation still keys on the exact anchor — the re-post exemption is neither broadened nor lost. Removing the newFindingKeys set is clean (single read site, the one replaced), and the lenient handling of a malformed start_line (drop it, keep the entry) matches the file's own precedent for line. No blockers in the diff.

The seam with the report's consumer — the bundled /review skill — is real, confirmed on current main again this pass, and now adjudicated by the subsystem owner rather than open: the documented findings-file shape is still [{"path":"src/foo.ts","line":42,"id":"R3-2"}] with no start_line anywhere in that section, Step 7's drop rule still matches on (path, line), and the --new-findings help still says "same-(path, line) overlap" — while the skill genuinely posts multi-line comments with start_line. @wenshao's real-environment verification (Aug 29) independently confirmed and deepened the same findings — the finding-side half is dormant until the contract changes; the live direction is the mirror case (a new point finding inside an existing multi-line comment), which his live A/B on a real PR shows working; and he additionally surfaced a placeholder-line false-positive class and two test-coverage gaps (the comment-side start_line read and the same-file guard are not pinned by any test). His stated conclusion: the change is safe to merge with no regressions, and the loop-closing work belongs in follow-ups. He then approved this commit. This gate deferred exactly that scope call to him, so the follow-ups are accepted scope tracked on #9219, not blockers:

  1. Make the overlap actionable — carry the matched range in existingComments.overlap and sync posting.md's Step 7 drop rule to it.
  2. Settle the finding-side half — either document and emit start_line in the findings file, or remove the dormant branch.
  3. Pin the live path — a comment-side-only start_line case and a cross-file case through the new predicate; plus refresh the --new-findings help text when that lands.

Test evidence

This is an unattended CI run — PR code is never executed here by policy. Evidence carried: the PR's own CI on the reviewed commit, read through the API — green across the board now (the previous pass found it sitting at action_required; the fork's workflow runs were since approved). The Test (ubuntu-latest, Node 22.x) job passing on this commit means the new range tests and the full presubmit suite hold in CI, independently of the author's own-machine numbers. Additionally, @wenshao ran the merge of this PR locally against a live scratch PR — 110/110 presubmit tests, whole review suite green, typecheck/lint/format clean, and a mutation matrix on the new predicate — that is the maintainer's own evidence, linked in this thread, and it goes well beyond what CI alone can say about this change.

Not verified by this gate: real-scenario behaviour, which this run does not drive — but the owner's live A/B above covers it, and a sandboxed @qwen-code /verify run is in flight right now on this commit; its report will post in this thread separately when it completes.

Final CI results for b8038ec (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

中文说明

代码审查

b8038ece 上复读——head 自首轮以来未变;本轮重新核对了 diff 与 main 上的消费方契约状态,而非沿用上一轮的结论。我对这个问题的独立方案与落地的形状一致:两端携带范围、按同文件闭区间相交分类、carried-id re-post 豁免保持精确锚点。

命令侧的改动正确且细致。 区间计算用 Math.min/Math.max 归一缺失/颠倒的端点,缺 start_line 时两端都退回点锚点——因此单行输入的行为与之前逐位一致,这正是大量既有 overlap/repost 测试所钉住的。区间相交是精确行相等的严格超集,原先落入 overlap 的不会逃逸,carriedIdsByLocation 仍按精确锚点建键——re-post 豁免既未放宽也未丢失。删除 newFindingKeys 集合很干净(唯一读点就是被替换的那个),对畸形 start_line 的宽容处理(丢弃该字段、保留条目)与本文件对 line 的先例一致。diff 中无阻塞性问题。

与报告消费方——内置 /review skill——的接缝真实存在,本轮在当前 main 上再次确认,现由子系统维护者裁定而非悬而未决: 文档化的 findings 文件形状仍是 [{"path":"src/foo.ts","line":42,"id":"R3-2"}],该章节内没有任何 start_line;Step 7 的删除规则仍按 (path, line) 匹配;--new-findings 帮助文本仍写着 "same-(path, line) overlap"——而 skill 确实会用 start_line 发布多行评论。@wenshao 的真实环境验证(8 月 29 日)独立确认并深化了同样的发现——finding 侧那一半在契约变更之前是休眠的;真正生效的是镜像情形(新的点 finding 落在既有多行评论内部),他在真实 PR 上的 A/B 证明其生效;他还额外发现了占位行误报类别与两处测试覆盖缺口(评论侧 start_line 读取与同文件守卫没有任何测试钉住)。他的结论:改动可安全合入、无回归,闭环工作作为后续跟进。随后他批准了这个 commit。本门正是把这一范围判断转交给他定夺的,因此这些跟进是记录在 #9219 下的已接受范围,不是阻塞项:

  1. 让 overlap 可执行——在 existingComments.overlap 中携带匹配到的区间,并同步 posting.md 的 Step 7 删除规则。
  2. 敲定 finding 侧那一半——要么在 findings 文件中记录并产出 start_line,要么移除休眠分支。
  3. 钉住真正会跑到的路径——一个仅评论带 start_line 的用例、一个走新谓词的跨文件用例;落地时同步刷新 --new-findings 帮助文本。

测试证据

这是无人值守的 CI 运行——按策略此处从不执行 PR 代码。本轮携带的证据:通过 API 读取的、该被审查 commit 上的 PR 自身 CI——现已全绿(上一轮时停在 action_required;该 fork 的工作流运行此后已获批准)。Test (ubuntu-latest, Node 22.x) 在此 commit 上通过,意味着新的区间测试与完整 presubmit 套件在 CI 中成立,独立于作者本机的数字。此外,@wenshao 在本机把本 PR 与 main 的合并产物对着一个真实临时 PR 运行过——110/110 presubmit 测试、整个 review 套件绿、typecheck/lint/格式干净,并对新谓词做了变异矩阵——那是维护者自己的证据,已链接在本线程中,其信息量远超 CI 本身能说明的。

本门未验证:真实场景行为(本次运行不驱动真机)——但上方维护者的实机 A/B 已覆盖,且一次沙箱 @qwen-code /verify 运行正在此 commit 上进行中,完成后报告会单独发布在本线程。

Qwen Code · qwen3.8-max

Reviewed at b8038ece96afd18bc0a1d5259eca3ff328e7cad9 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — correct, conservative, and well-tested within its stated scope; CI is green on this commit, and the known consumer-contract gaps were reviewed and accepted by the subsystem owner as tracked follow-ups.

The previous pass deferred here with two questions for the subsystem owner; both are now settled. The fork's CI runs were approved and came back green on b8038ece — unit, lint-adjacent gates, desktop shell, secret scan, all of it — so there is finally something to certify against, and it certifies. The scope question — whether the skill-side consumer contract rides along or lands as a follow-up — was answered the thorough way: @wenshao built the merge of this PR, drove it against a live PR carrying real multi-line comments, ran a mutation matrix over the new predicate, confirmed the seam gaps this review flagged (and found two more), concluded the change is safe to merge with no regressions, and approved. That is the human call this gate exists to hand off to, and second-guessing it now would be noise, not gatekeeping.

Stepping back on the diff itself with fresh eyes: the shape is right, and the conservative edges are all deliberate — single-line behavior is provably unchanged, the re-post exemption stays exact-anchor, a malformed start_line degrades a finding to its pre-PR semantics instead of corrupting the parse. If I were maintaining presubmit.ts in six months, the range math would read fine. The named nits keeping this at 4 rather than 5 are the accepted follow-ups, all tracked on #9219: make the overlap entry carry the matched range so Step 7's drop rule can act on it; settle the dormant finding-side half (document start_line in the findings file or remove the branch); and pin the comment-side path that actually fires in production, which today survives mutation. None of those are defects in this diff — they are the second half of a two-half fix, and the owner has them.

Approving, pinned to the reviewed commit. @bluefateludi — a disciplined change that knows exactly what it is and isn't; thanks for scoping it that way.

中文说明

信心度:4/5 —— 在其声明范围内正确、保守且测试充分;该 commit 上 CI 已全绿,已知的消费方契约缺口经子系统维护者审查并接受为记录在案的后续跟进。

上一轮在此暂缓,留给子系统维护者两个问题;现已全部落定。该 fork 的 CI 运行获准执行,并在 b8038ece 上全绿——单元、相关门禁、桌面壳、密钥扫描全部通过——终于有了可认证的对象,且认证通过。范围问题——skill 侧消费契约随本 PR 一起还是作为后续跟进——以最扎实的方式给出了答案:@wenshao 构建了本 PR 的合并产物,对着携带真实多行评论的实际 PR 运行,对新谓词做了变异矩阵,确认了本审查指出的接缝缺口(还额外发现两处),得出改动可安全合入且无回归的结论,并已批准。这正是本门要转交的人类判断;此时再质疑只是噪音,不是把关。

以新视角退一步看 diff 本身:形状正确,保守之处皆有意为之——单行行为可证明地不变,re-post 豁免保持精确锚点,畸形 start_line 让 finding 退回 PR 之前的语义而不是破坏解析。若六个月后由我维护 presubmit.ts,这段区间计算读起来没有问题。使其停在 4 而非 5 的点名事项,是已接受的后续跟进,均记录在 #9219:让 overlap 条目携带匹配到的区间,使 Step 7 的删除规则可据其处置;敲定休眠的 finding 侧那一半(在 findings 文件中记录 start_line 或移除该分支);钉住生产中真正会跑到的评论侧路径——它今天能在变异下存活。这些都不是本 diff 的缺陷——它们是一个两半修复的第二半,维护者已有安排。

批准,锚定在被审查的 commit。@bluefateludi —— 一次有章法的改动,清楚知道自己是什么、不是什么;感谢这样的范围划分。

Qwen Code · qwen3.8-max

Reviewed at b8038ece96afd18bc0a1d5259eca3ff328e7cad9 · re-run with @qwen-code /triage

@bluefateludi

Copy link
Copy Markdown
Contributor Author

Thanks for the review. This PR intentionally focuses on command-side range support. I can address the /review skill integration in a follow-up, or include it here if maintainers prefer.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline.

Not explored to full depth (tool budget reached): "agent 1a": run packages/cli presubmit.test.ts under vitest (blocked: no node_modules in worktree; full install not attempted); "agent 1a": npm run typecheck over the changed files (blocked: same missing-dependency condition).

中文说明

已审查。 建议见行内评论。

未探索到全部深度(达到工具调用预算):"agent 1a"run packages/cli presubmit.test.ts under vitest (blocked: no node_modules in worktree; full install not attempted)"agent 1a"npm run typecheck over the changed files (blocked: same missing-dependency condition)

— qwen3.8-max via Qwen Code /review (v0.22.0)

out.push({
path: e.path,
line: typeof e.line === 'number' ? e.line : 0,
...(typeof e.start_line === 'number' ? { startLine: e.start_line } : {}),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-3: The new start_line parse arm validates nothing beyond typeof === 'number', and both directions of that leniency silently corrupt the new range-overlap dedup while findingsFileInvalid stays false (the report reads as a clean pass).

Two shapes, both probed on this commit:

  • Wrong type collapses the range: [{"path":"a.ts","start_line":"12","line":18}] parses to the point [18,18] (the key is silently dropped), so an existing comment spanning lines 12–17 no longer intersects → noConflict, and a duplicate posts. The numeric control arm correctly reports overlap.
  • Out-of-domain widens it: start_line: -3 yields [-3,18], flipping an unrelated same-file comment at line 5 to overlap and blockOnExistingComments to true — a wrong premise handed to the deterministic drop rule; fractional 2.5 misses dedup at line 2. Pre-diff start_line was not parsed at all, so both shapes were inert.

Probe output (unmodified PR code):

parseFindingsFile('[{"path":"a.ts","start_line":"12","line":18}]') → [{path:'a.ts',line:18}]
string arm:    byBucket {overlap:0, noConflict:1}, blockOnExistingComments:false
numeric arm:   byBucket {overlap:1},               blockOnExistingComments:true
start_line:-3: unrelated line-5 comment → overlap   start_line:2.5 → line 2 escapes dedup
with a domain gate (positive safe integer, else reject whole file): both arms → findingsFileInvalid:true

The file's own fail-safe rejects the WHOLE file for a misshapen id because a corrupt id would actively corrupt a match — a corrupt range does the same here, and submit.ts's isDiffLine (Number.isSafeInteger(n) && n > 0) is the existing domain gate for exactly this value class on the posting side. Suggested fix: mirror the id fail-safe on the new arm — when start_line is present (and not null) but not a positive safe integer, reject the whole file (return null), e.g. by reusing/exporting isDiffLine; or, if the lenient drop is intentional, a comment saying so keeps the next reader from re-deriving this.

中文说明

新的 start_line 解析分支只校验 typeof === 'number',两个方向的宽松都会静默破坏新的区间去重,而 findingsFileInvalid 保持 false(报告读起来像一次干净的通过)。

两种形态,均在本提交上以探针实测:

  • 类型错误使区间塌缩:[{"path":"a.ts","start_line":"12","line":18}] 被解析为点 [18,18](该键被静默丢弃),横跨 12–17 行的既有评论不再与之相交 → 判为 noConflict,重复评论被发出;数字对照组正确给出 overlap
  • 域外数值放小区间:start_line: -3 得到 [-3,18],同文件第 5 行一条无关评论被翻转为 overlap,blockOnExistingComments 置 true——向确定性丢弃规则提供了错误前提;小数 2.5 则漏掉第 2 行的去重。diff 之前 start_line 根本不被解析,两种形态都是惰性的。

本文件自身的 fail-safe 先例是:畸形 id 拒绝整个文件,因为损坏的 id 会主动破坏匹配——损坏的区间在这里同理;submit.tsisDiffLine(Number.isSafeInteger(n) && n > 0)正是发布侧针对这一数值类型的既有域校验。建议修复:对新分支镜像 id 的 fail-safe——start_line 存在(且非 null)但不是正安全整数时拒绝整个文件(return null),例如复用/导出 isDiffLine;若宽松丢弃是有意为之,加一条注释说明,避免后续读者重新推导。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +698 to +700
const commentLine = c.line ?? 0;
const commentStartLine = c.start_line ?? commentLine;
const commentRangeStart = Math.min(commentStartLine, commentLine);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-1: No test discriminates the comment-side start_line read here. A mutant that ignores c.start_line (commentStartLine := commentLine) passes the entire suite — measured in a scratch-tree run: unmodified code 109/109 pass, same mutant 109/109 pass. In the two new tests that give a comment a start_line, the outcome already holds through the comment's END line alone ([8,14] intersects [12,18] via 14; [4,8] stays disjoint as [8,8]).

The regression that could then ship: an existing multi-line comment {start_line: 8, line: 20} with a new finding range [12,18] overlaps only via the comment's start line; with c.start_line ignored it classifies noConflict, the gate misses the duplicate, and the finding re-posts as a visible duplicate — the exact #9219 bug on the comment side. The finding-side counterpart IS discriminated, which makes this asymmetry easy to miss.

One test closes it, and it flips the mutant as required (fails under the mutant with expected +0 to be 1, passes on the real code): a comment { id, path: 'a.ts', start_line: 8, line: 20, commit_id: 'abc123', user: { login: 'qwen-code-ci-bot' } } with a **[Critical]** body against findings [{ path: 'a.ts', start_line: 12, line: 18 }], expecting byBucket.overlap 1 / byBucket.noConflict 0.

中文说明

此处评论侧的 start_line 读取没有任何测试可以区分:忽略 c.start_line 的变异体(commentStartLine := commentLine)能通过整个套件——在临时树中实测:未改动代码 109/109 通过,同一变异体同样 109/109 通过。两个给评论带 start_line 的新测试里,结果仅凭评论的结束行就已成立([8,14] 经由 14 与 [12,18] 相交;[4,8] 收缩为 [8,8] 后仍不相交)。

由此可能溜进发布的回归:既有多行评论 {start_line: 8, line: 20} 与新 finding 区间 [12,18] 仅经由评论的起始行相交;若 c.start_line 被忽略,则判为 noConflict,overlap 门漏掉重复,finding 被再次发出——正是 #9219 要修的 bug,只是发生在评论侧。finding 侧的对应读取是有测试区分的,这种不对称很容易被忽略。

补一个测试即可闭合,且它能按预期翻转变异体(在变异体下以 expected +0 to be 1 失败,在真实代码下通过):评论 { id, path: 'a.ts', start_line: 8, line: 20, commit_id: 'abc123', user: { login: 'qwen-code-ci-bot' } }(带 **[Critical]** 正文),findings 为 [{ path: 'a.ts', start_line: 12, line: 18 }],断言 byBucket.overlap 为 1、byBucket.noConflict 为 0。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +702 to +703
const overlapsNewFinding = newFindings.some((finding) => {
if (finding.path !== (c.path ?? '')) return false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-2: The overlap gate became range-based while the comment set it runs over is still recognized partly by an ungated, any-account shape match: the qwenComments filter accepts any comment whose body contains the short footer substring (via Qwen Code /review), with no account condition (~line 936). Pre-diff, exact ${path}:${line} keying meant one planted comment suppressed exactly its single anchor line; with range intersection, one forged multi-line comment silences every new finding whose range intersects it.

Measured on this commit: a comment from an account named attacker spanning a.ts lines 10–60 with the footer substring in the body, against a new finding at line 35 → byBucket.overlap: 1, blockOnExistingComments: true. Any user who can comment on the PR can plant one; the commit_id stale gate constrains them exactly as before (post at the current head), so feasibility is unchanged — only the width per plant grew. The drop log names the responsible comment after the fact but does not prevent a genuinely new Critical in that range from being withheld.

Suggested fix: restrict the new range-intersection branch to account-gated comments (the marker/severity disjuncts, where provenance is the posting account) and keep the any-account footer match exact-line as before; or explicitly document that an ungated footer match now grants range-wide suppression.

中文说明

overlap 门变为基于区间,而它作用的评论集合仍部分依赖一个不设账户门槛的形状匹配来识别:qwenComments 过滤器接受任何正文包含短尾注子串(via Qwen Code /review)的评论,没有账户条件(约第 936 行)。diff 之前,精确的 ${path}:${line} 键意味着一条植入的评论只能压制其锚点所在的那一行;改为区间相交后,一条伪造的多行评论可以压制所有与其区间相交的新 finding。

在本提交上实测:一个名为 attacker 的账号发布横跨 a.ts 10–60 行、正文含尾注子串的评论,对第 35 行的新 finding → byBucket.overlap: 1blockOnExistingComments: true。任何能在 PR 上评论的用户都可以植入这样一条评论;commit_id 过期门槛对攻击者的约束与之前完全一致(发在当前 head 即可),可行性未变——只是每条植入评论的压制宽度变大了。丢弃日志事后会点名该评论,但无法阻止该区间内真正的新 Critical 被扣下。

建议修复:将新的区间相交分支限制为有账户门槛的评论(marker/severity 两个分支,其来源是发布账号),任一账户的尾注匹配保持原有的精确行语义;或明确注明:无账户门槛的尾注匹配现在授予区间级压制。

— qwen3.8-max via Qwen Code /review (v0.22.0)

@wenshao

wenshao commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Local real-environment verification report

I built the merge of this PR with current main and ran it as a real qwen review presubmit against a real GitHub PR carrying real multi-line review comments, plus a same-tree A/B and a mutation matrix. Posting the findings here as a merge reference.

Verdict — the code is correct and safe, but the loop is not closed. The behaviour change is real and it fires on ~1 in 5 of our actual review comments. Nothing regresses: exact-line matching, the carried-id re-post exemption, the same-file guard and disjoint ranges are all unchanged, and no finding that used to be dropped stops being dropped. What I could not confirm is that the change produces the outcome the description claims: the half of it the description leads with is unreachable in production today, and the half that does fire raises blockOnExistingComments=true that Step 7 has no way to act on. Details and repro below.

Environment

Tree b8038ece96 (PR head) merged with origin/mainclean merge, both hunks survive; presubmit.ts/presubmit.test.ts/posting.md untouched on main since
Platform macOS 26.6.2 (arm64), Node v24.18.1, npm 11.16.0, Vitest 3.2.7
Real target wenshao/qwen-code#7 — a scratch PR in my own fork with three real inline comments posted through POST /pulls/{n}/comments, each carrying the genuine _— … via Qwen Code /review (v0.22.3)_ footer
Binary under test npm run build && npm run bundledist/cli.js, run as qwen review presubmit (no mocks, no stubs, real gh api)

1. Static checks — all green

  • npx vitest run src/commands/review/presubmit.test.ts110/110 pass (109 on the PR's base; main added one since).
  • Whole review command suite npx vitest run src/commands/review/116 files, 5634 pass, 3 skipped. (One unrelated [vitest-worker]: Timeout calling "onTaskUpdate" unhandled error — the known worker-RPC flake, not caused by this PR.)
  • npm run typecheck exit 0; npx eslint … --max-warnings 0 exit 0; npx prettier --check clean; git diff --check clean.
  • Reverting only the behavioural hunk (keeping the tests) fails exactly 2 of the 3 new tests, so they are genuine regression tests; the third (keeps disjoint ranges in noConflict) is a control that passes both ways. That is correct and worth saying out loud.

2. Same-tree A/B on a real GitHub PR

Two bundles from one tree — dist-pr (this PR) and dist-base (this PR's behavioural hunk reverted, nothing else changed) — pointed at the same live PR and the same findings files.

A/B matrix

The real multi-line comment the fix keys on — GitHub really does return start_line: 7 for it:

real multi-line review comment

P1 is the fix working, on a real comment, through the real gh path. P2 / P6 / P7 / P8 are unchanged controls: exact-line match, the carried-id re-post exemption (repost=1 on both sides — the exemption is not broadened or lost), the same-file guard, and the disjoint case.

Reachability is real, not theoretical. Across 8 recent PRs, 97 of 455 Qwen /review inline comments (21.3%) are multi-line, i.e. carry a start_line:

PR qwen inline comments multi-line
#9740 334 80 24.0%
#10390 32 7 21.9%
#10423 45 2 4.4%
#10301 26 2 7.7%
#10427 9 2 22.2%
#10421 6 2 33.3%
#9801 3 2 66.7%
total 455 97 21.3%

Their spans: median 3 lines, p90 10, max 141 (this one, revert-hunk.test.ts 1840–1980). So each such comment now claims a whole range as its overlap zone, and the widest one claims 141 lines of a file.

3. Gap A — the direction the description leads with cannot happen in production

The description says the bug is noConflict "when an existing comment sat inside a newly drafted multi-line finding". That direction needs finding.startLine, which comes only from start_line in the --new-findings file — and nothing writes it:

  • skills/bundled/review/references/posting.md, the findings-file section: the documented shape is still [{"path":"src/foo.ts","line":42,"id":"R3-2"}, ...]. Zero mentions of start_line anywhere in that section (checked against the shipped dist/bundled/review/).
  • The subcommand's own --new-findings help still reads: "a JSON file shaped as [{path, line, id?}, ...] — … existing comments are checked for same-(path, line) overlap".

My probe P3 only flips because I hand-wrote start_line into the findings file. With the file the orchestrator actually writes, finding.startLine is always undefined, findingRange collapses to a point, and parseFindingsFile's new branch never executes. What genuinely fires today is the mirror case: a new single-line finding landing inside an existing multi-line comment (P1) — a real and worthwhile fix, but not the one written up.

Two of the three new tests (classifies a comment inside a new finding range, classifies intersecting … ranges) set start_line on the finding, i.e. they pin the unreachable direction.

4. Gap B — the overlap fires, but Step 7's drop rule has no target

drop rule gap

CommentSummary carries only line: c.line ?? 0 — the comment's end line. It carries neither the comment's start_line nor which finding matched. Under the old exact-line rule those coincided by construction, which is what made posting.md's Step 7 rule work:

Drop each finding whose (path, line) appears in existingComments.overlap from your comments array

Under range matching they no longer coincide. In P1 the report hands back {"path":"verify-9801-fixture.ts","line":14} while the finding is at line 10 — so the drop set is empty, the duplicate is posted anyway, and blockOnExistingComments=true tells the model to block with nothing to drop. The drop-log line the rule prescribes (already reported at <path>:<line>) would also name a location no finding of this round occupies.

posting.md is explicit that this decision point must stay deterministic ("this has been improvised as an interactive question, which stalls a headless run forever"). A block with an empty drop set is exactly the state that invites improvisation.

5. Gap C — a new false-positive class, from posting.md's own instructions

posting.md tells the orchestrator to include body-only Criticals in the findings file with a placeholder line:

include one {path, line} per body Critical (any placeholder line, e.g. 1, and NO id …)

P5 does exactly that — [{"path":"…","line":1}] — against an unrelated existing comment spanning lines 1–3. Base: noConflict. This PR: overlap=1, blockOnExistingComments=true. A placeholder that was chosen precisely because it means nothing now aliases into any multi-line comment that happens to cover line 1, and produces a block with (again) nothing droppable.

6. Gap D — the production-reachable half is untested

Mutants applied to presubmit.ts, each run against the whole 110-test file:

mutation matrix

mutant change verdict
M1 <=< on both bounds KILLED (32)
M2 drop Math.min/max on the comment range SURVIVED
M3 ignore finding.startLine KILLED (2)
M4 ignore c.start_line SURVIVED
M5 delete the same-file guard (finding.path !== c.path) SURVIVED
M6 drop Math.min/max on the finding range SURVIVED
M7 parseFindingsFile drops start_line KILLED (3)

M4 is the important one. Deleting the comment-side start_line read — the only half of this PR that runs in production today — leaves all 110 tests green. M5 shows the same-file guard is unpinned too: the old code got path-safety for free from the ${path}:${line} key; the new predicate makes it an explicit if that no test exercises. (Runtime behaviour is correct — P7 confirms it on the real rig — it is the test that is missing.) M2/M6 are lower stakes: submit validates start_line <= line before posting and GitHub never returns a reversed range, so the normalization is defence-in-depth.

Recommendation

The change is safe to merge and I found no regression. But as it stands it broadens a detector without moving the consumer contract, so the outcome it advertises — a duplicate that stops being posted — does not actually happen. Three small follow-ups would close it, and they belong in this PR rather than a new one:

  1. Make the overlap actionable. Carry the matched finding's anchor (or at least the comment's start_line/range) in existingComments.overlap[], and update posting.md's Step 7 drop rule and drop-log wording to key on it instead of (path, line). Without this, blockOnExistingComments gains cases it cannot dispose of.
  2. Pick a side on the finding half. Either update posting.md's findings-file spec and the --new-findings help to emit and describe start_line (then the PR's headline scenario becomes real), or drop FindingAnchor.startLine + the parseFindingsFile branch as dead code. Today the code and the documented contract disagree.
  3. Pin what actually runs. Add a case where only the comment carries start_line (kills M4), and a cross-file case through the new predicate (kills M5). Optionally decide what a body-only placeholder anchor should do (Gap C) — excluding id-less placeholder anchors from range matching, or telling posting.md to place them out of range, would both work.

Happy to be talked out of any of this — everything above is reproducible from wenshao/qwen-code#7 and the probe files, and I'm glad to hand those over.

中文版本

本地真实环境验证报告

我把本 PR 与当前 main 合并后完整构建,用真实的 qwen review presubmit真实 GitHub PR 上真实的多行 review 评论,并做了同树 A/B 与变异矩阵。结论发在这里作为合并参考。

结论 —— 代码正确且安全,但链路没有闭合。 行为变更是真实的,在我们实际的 review 评论里约 1/5 会触发。没有任何回归:精确行匹配、carried-id re-post 豁免、同文件守卫、不相交区间全部不变,原本会被丢弃的 finding 也不会变成不丢弃。我没能确认的是它能否产出描述里声称的结果:描述主打的那半边在当前生产环境根本走不到,而真正会触发的那半边会把 blockOnExistingComments 置为 true,但 Step 7 对它无从下手。

环境

代码树 b8038ece96(PR head)与 origin/main 合并 —— 无冲突,两处 hunk 都存活;此后 main 未再改动 presubmit.ts/presubmit.test.ts/posting.md
平台 macOS 26.6.2 (arm64)、Node v24.18.1、npm 11.16.0、Vitest 3.2.7
真实目标 wenshao/qwen-code#7 —— 我自己 fork 里的临时 PR,三条评论全部通过 POST /pulls/{n}/comments 真实发出,都带真实的 _— … via Qwen Code /review (v0.22.3)_ 页脚
被测二进制 npm run build && npm run bundledist/cli.js,以 qwen review presubmit 运行(无 mock、无桩,真实 gh api

1. 静态检查 —— 全绿

  • npx vitest run src/commands/review/presubmit.test.ts110/110 通过(PR base 上是 109,main 后来加了一个)。
  • 整个 review 命令套件 npx vitest run src/commands/review/116 个文件、5634 通过、3 skipped。(另有一条与本 PR 无关的 [vitest-worker]: Timeout calling "onTaskUpdate" 未处理错误,是已知的 worker RPC 抖动。)
  • npm run typecheck 退出 0;npx eslint … --max-warnings 0 退出 0;npx prettier --check 通过;git diff --check 干净。
  • 只回退行为 hunk(保留测试),3 个新测试中恰好 2 个失败,说明它们是真的回归测试;第三个(keeps disjoint ranges in noConflict)是两边都通过的对照。这点值得明说。

2. 真实 GitHub PR 上的同树 A/B

同一棵树出两个 bundle —— dist-pr(本 PR)与 dist-base(只回退本 PR 的行为 hunk,其余完全一致)—— 指向同一个线上 PR、同一批 findings 文件。

A/B 矩阵

修复所依赖的那条真实多行评论 —— GitHub 确实返回了 start_line: 7

真实多行 review 评论

P1 就是修复在真实评论、真实 gh 链路上生效P2 / P6 / P7 / P8 是不变的对照组:精确行匹配、carried-id re-post 豁免(两侧都是 repost=1,豁免既没被放宽也没丢失)、同文件守卫、不相交区间。

可达性是实测的,不是推测。 最近 8 个 PR 里,455 条 Qwen /review 行内评论中有 97 条(21.3%) 是多行的,即带 start_line

PR qwen 行内评论 多行
#9740 334 80 24.0%
#10390 32 7 21.9%
#10423 45 2 4.4%
#10301 26 2 7.7%
#10427 9 2 22.2%
#10421 6 2 33.3%
#9801 3 2 66.7%
合计 455 97 21.3%

跨度:中位数 3 行,p90 10 行,最大 141 行这一条revert-hunk.test.ts 1840–1980)。也就是说每条这样的评论现在都会把整个区间圈成自己的 overlap 区,最宽的一条圈掉了 141 行。

3. 缺口 A —— 描述主打的那个方向在生产环境走不到

描述说 bug 是「既有评论位于新起草的多行 finding 内部」时报 noConflict。这个方向需要 finding.startLine,而它只能来自 --new-findings 文件里的 start_line —— 但没有任何地方会写它:

  • skills/bundled/review/references/posting.md 的 findings 文件章节,文档形状仍然是 [{"path":"src/foo.ts","line":42,"id":"R3-2"}, ...],该章节内 start_line 出现 0 次(对已打包dist/bundled/review/ 核对过)。
  • 子命令自己的 --new-findings 帮助文本仍写着:"a JSON file shaped as [{path, line, id?}, ...] — … existing comments are checked for same-(path, line) overlap"

我的探针 P3 之所以能翻转,只是因为我手工往 findings 文件里写了 start_line。用 orchestrator 实际写出的文件,finding.startLine 恒为 undefined,finding 区间退化成一个点,parseFindingsFile 的新分支永远不执行。今天真正会触发的是镜像情形:新的单行 finding 落进既有的多行评论内部(P1)—— 这是真实且有价值的修复,但不是被写进描述的那个。

3 个新测试里有 2 个(classifies a comment inside a new finding rangeclassifies intersecting … ranges)是在 finding 上设 start_line,也就是在钉那个走不到的方向。

4. 缺口 B —— overlap 触发了,但 Step 7 的丢弃规则找不到目标

丢弃规则缺口

CommentSummary 只带 line: c.line ?? 0,即评论的结束行;既不带评论的 start_line,也不带是哪个 finding 匹配上的。在旧的精确行规则下这两者按构造相等,posting.md 的 Step 7 规则正是靠这一点成立:

Drop each finding whose (path, line) appears in existingComments.overlap from your comments array

改成区间匹配后两者不再相等。P1 里报告回传的是 {"path":"verify-9801-fixture.ts","line":14},而 finding 在第 10 行 —— 于是丢弃集合为空,重复评论照发,同时 blockOnExistingComments=true 又要求模型阻塞。规则要求的丢弃日志行(already reported at <path>:<line>)也会指向本轮任何 finding 都不在的位置。

posting.md 明确要求这个决策点必须保持确定性("this has been improvised as an interactive question, which stalls a headless run forever")。「阻塞但没有可丢弃项」恰好就是最容易诱发即兴发挥的状态。

5. 缺口 C —— 一个新的误报类别,来自 posting.md 自己的指示

posting.md 要求 orchestrator 把 body-only Critical 也写进 findings 文件,用占位行号:

include one {path, line} per body Critical (any placeholder line, e.g. 1, and NO id …)

P5 就照着做了 —— [{"path":"…","line":1}] —— 对上一条无关的、跨越 1–3 行的既有评论。base:noConflict。本 PR:overlap=1, blockOnExistingComments=true。一个正因为「不表示任何含义」才被选中的占位行号,现在会跟任何恰好覆盖第 1 行的多行评论对上,并且(同样)产生一个没有可丢弃项的阻塞。

6. 缺口 D —— 生产环境唯一会跑到的那半边没有测试

presubmit.ts 上施加变异,每个变异体都跑完整的 110 个测试:

变异矩阵

变异体 改动 结论
M1 两个边界 <=< 被杀(32 失败)
M2 去掉评论区间的 Math.min/max 存活
M3 忽略 finding.startLine 被杀(2 失败)
M4 忽略 c.start_line 存活
M5 删掉同文件守卫finding.path !== c.path 存活
M6 去掉 finding 区间的 Math.min/max 存活
M7 parseFindingsFile 丢掉 start_line 被杀(3 失败)

M4 是关键。 把评论侧读取 start_line 整个删掉 —— 也就是本 PR 今天在生产环境唯一会跑到的那半边 —— 110 个测试仍然全绿。M5 说明同文件守卫同样没被钉住:旧代码靠 ${path}:${line} 这个 key 免费获得路径安全,新写法把它变成一个显式 if,而没有测试走到它。(运行时行为是对的,P7 在真机上确认过;缺的是测试。)M2/M6 风险较低:submit 在发帖前会校验 start_line <= line,GitHub 也不会返回反向区间,所以这层归一化属于纵深防御。

建议

改动可以安全合并,我没有发现回归。但就目前状态而言,它只拓宽了检测器,没有同步消费方契约,所以它宣称的效果 —— 重复评论不再被发出 —— 实际上并没有发生。三个小的后续可以把它闭合,且应该放在本 PR 里而不是新开一个:

  1. 让 overlap 可执行。existingComments.overlap[] 里带上匹配到的 finding 锚点(至少带上评论的 start_line/区间),并同步更新 posting.md 的 Step 7 丢弃规则与丢弃日志措辞,让它按这个新字段而不是 (path, line) 去匹配。否则 blockOnExistingComments 会多出一批无法处置的情形。
  2. 在 finding 那半边二选一。 要么更新 posting.md 的 findings 文件规格以及 --new-findings 帮助文本,让它产出并描述 start_line(这样 PR 的主打场景才真实存在);要么把 FindingAnchor.startLineparseFindingsFile 的对应分支作为死代码删掉。当前代码与文档化契约是不一致的。
  3. 钉住真正会跑的路径。 补一个只有评论start_line 的用例(杀掉 M4),以及一个走新谓词的跨文件用例(杀掉 M5)。可选地为 body-only 占位锚点定个说法(缺口 C)—— 把无 id 的占位锚点排除出区间匹配,或在 posting.md 里要求把它们放到不会落入区间的位置,两种都行。

以上任何一条都欢迎反驳 —— 全部结论都可以从 wenshao/qwen-code#7 和探针文件复现,需要的话我可以把它们交出来。

@wenshao

wenshao commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao enabled auto-merge August 31, 2026 17:12
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 461 passed · 0 failed · 461 total

Flakiness gate: ✅ 1 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:461 通过 · 0 失败 · 461 总计

抖动门:✅ 1 changed test file(s) x 5 identical rounds, no divergence

Verification report

PR 9801 — fix(cli): detect overlapping review comment ranges

Verdict: merge-ready — 461/461 scripted assertions passed, 0 unexpected failures. Verified head: b8038ece96afd18bc0a1d5259eca3ff328e7cad9 (base tip 88a136317a3bff866cb1b07c50cd530ba95ac39c). Two non-blocking findings below (one Suggestion about production reachability of the finding-side half, one nit on a stale test count in the body).

中文摘要
  • 结论merge-ready。461 条脚本化断言全部通过,0 个意外失败。
  • A/B 结论:在 base 构建上,5 个区间相交场景(点评论位于新多行 finding 内、多行既有评论覆盖单行 finding、部分相交、端点相触、反序区间)全部被误判为 noConflict(bug 复现);在 head 构建上全部翻转为 overlap。单行精确匹配、不相交、相邻区间、跨文件路径门、carried-id re-post 精确锚点豁免在两侧行为一致(12 场景 × 2 臂 = 24 单元,240 条断言,见 01-ab-range-overlap-base-vs-head.png)。
  • 变异矩阵:分类器 hunk 单独还原 → 2 个新测试红(非空转);start_line 解析行单独删除 → 3 红;谓词置 false 的正对照 → 34 红(套件确实抓得住该文件);双 hunk 同时还原 → 与单删解析行相同的 3 红,无组合才暴露的隐患(02-mutation-matrix.png)。
  • Findings(均不阻塞):① finding 侧 start_line 在生产流程中暂无写入方——bundled review skill 的 posting.md 指示 new-findings 文件只写 {path, line, id?},因此 PR 标题场景(既有评论位于新多行 finding 内)在 shipped skill 下不可达;真正生产可达且已修复的是反方向(GitHub 多行既有评论 vs 单行新 finding)。建议同 PR 更新 posting.md 的 schema 或记入 /review presubmit overlap matching is exact-line only — multi-line ranges and semantic duplicates pass as noConflict #9219 跟进。② PR 正文称 109 个测试,实际 head 为 110(base 106 + 3 分类 + 1 解析 fixture)。
  • 未覆盖:Aone 路径无区间数据可消费(AoneMrComment 类型无 range 字段,行为与 base 相同,非回归);语义正文匹配与邻近评论 bucket(正文声明由 /review presubmit overlap matching is exact-line only — multi-line ranges and semantic duplicates pass as noConflict #9219 跟踪);对真实 GitHub 的 E2E(无 token,harness 以 fake gh peer 编码上游语义代替)。

Central claim and A/B proof

Central claim: an existing same-file inline comment whose line range intersects a new finding's range (closed-interval) is classified overlap (and therefore deterministically dropped) instead of noConflict; previously only exact (path, line) equality was compared.

Harness: the real compiled qwen review presubmit subcommand on each arm (head packages/cli/dist, base rebuilt from HEAD^1 in a scratch worktree) driven through a fake gh peer (harness/bin/gh) that serves fixture JSON for the four endpoints presubmit queries (pulls/{n}, user, check-runs, pulls/{n}/comments) and logs every invocation. No mocks of the code under test; findings reach it through the real --new-findings file and parseFindingsFile. Control integrity: the compiled base↔head presubmit.js diff is exactly the PR's hunks (34 diff lines); reference packages are byte-identical across the PR (git diff HEAD^1..HEAD over them is empty), and node_modules/@qwen-code/qwen-code-core resolves to the unchanged core on both arms.

# scenario (comment vs new finding, same file unless noted) base head expectation met
S1 point comment 15 inside finding 12–18 (PR headline) noConflict overlap yes
S2 multi-line comment 12–18 vs point finding 15 (production-reachable direction) noConflict overlap yes
S3 partial intersection 8–14 vs 12–18 noConflict overlap yes
S4 disjoint 4–8 vs 12–18 noConflict noConflict yes
S5 exact single line 15 vs 15 overlap overlap unchanged ✓
S6 single-line disjoint 15 vs 20 noConflict noConflict unchanged ✓
S7 adjacent 12–14 vs 15–18 noConflict noConflict yes
S8 touching at endpoint 12–15 vs 15–18 noConflict overlap yes
S9 carried-id repost at exact anchor (15, id R2-1) overlap+repost overlap+repost unchanged ✓
S10 carried id at a different end line (comment 12–18, finding 15) noConflict overlap, no repost range overlap does not broaden thread reuse ✓
S11 same line, different file noConflict noConflict path gate ✓
S12 reversed finding range (start_line 18, line 12) noConflict overlap normalized ✓

Each cell additionally asserted 4 validity controls (comment recognized, CI all_pass, not self-PR, no drift) and 2 wire assertions (comments endpoint called exactly once, compare endpoint never called) — 10 assertions/cell × 24 cells = 240/240. Witness: evidence/01-ab-range-overlap-base-vs-head.png.

Secondary claims hold: exact single-line behavior unchanged (S5/S6), repost exemption remains exact-line + id-based (S9/S10 — a range-intersecting comment whose end line differs from the carried anchor lands in overlap but never repost).

Corrections to the description

  1. Test count. The body claims "109/109". The file at the verified head runs 110 tests (base file runs 106; the PR adds 3 classification tests and 1 parse fixture row). All pass on both arms' suites (head 110/110, base 106/106).
  2. Production reachability of the headline scenario. The body's motivating case (existing comment inside a newly drafted multi-line finding) requires the --new-findings file to carry start_line. The only production producer of that file is the bundled review skill, whose references/posting.md instructs the orchestrator to write [{path, line, id?}] only (its echo example and the drift-section instruction both omit start_line). So in the shipped flow finding.startLine is always undefined and the finding-side half of the intersection is dormant; the production-reachable, fixed direction is the comment side (GitHub multi-line comments carry start_line, now parsed — S2). The code-level claim is proven either way; this is a description/scope correction, not a code defect.

Findings (non-blocking)

F1 (Suggestion) — finding-side start_line has no producer in the shipped flow. parseFindingsFile accepts start_line and the classifier consumes startLine, but no in-repo writer emits it: posting.md's new-findings schema is {path, line, id?}, and nothing else writes the file. Consequence: the PR's own regression tests exercise a shape production never produces, and the headline dedup gap persists for drafted multi-line findings until the skill doc gains the field (the resolver already computes startLine for the comments payload, so the data is one doc sentence away). The comment-side half is independently valuable and load-bearing (S2 flips on base). Suggested disposition: add start_line to posting.md's schema in this PR, or track in #9219 and say so in the body. Not blocking: behavior is correct for every shape a producer can emit today, and the dormant half is fail-safe (point semantics, identical to base).

F2 (nit) — stale test count in the body (109 vs 110; see Corrections).

Mutation matrix (vacuity)

Witness: evidence/02-mutation-matrix.png. Unmutated controls green on both arms (head 110/110, base 106/106).

mutation failed | passed red set verdict
M1 classifier reverted to base point-equality 2 | 108 exactly the 2 NEW range tests, expected +0 to be 1 new classification tests non-vacuous; zero collateral reds
M2 drop start_line parse line 3 | 107 2 NEW range tests + NEW parse fixture (the test helper round-trips findings through parseFindingsFile) parse hunk load-bearing
M3 predicate → false (positive control) 34 | 76 every pre-existing overlap/repost test plus the new ones the suite provably catches mutations in this file
M4 M1+M2 combined 3 | 107 identical to M2's union; no combo-only red no hidden layered hazard

Not covered

  • Aone multi-line comments: aoneCommentToPresubmitComment maps no range because the AoneMrComment payload type carries none (checked lib/platform/aone.ts); Aone keeps point semantics exactly as at base — unchanged, not a regression. If a1 ever grows multi-line anchors, the mapping needs the same field.
  • Semantic body matching, full-body rendering, nearby-comment bucket — declared out of scope by the body, tracked by /review presubmit overlap matching is exact-line only — multi-line ranges and semantic duplicates pass as noConflict #9219; not probed.
  • Live GitHub E2E — no token in this environment by design; the fake gh peer encodes the four endpoints' response shapes (post-jq for the two --jq calls, merged-array for --paginate), which proves the handling, not the trigger: a real multi-line comment's start_line provenance is GitHub-documented but not re-verified here.
  • Repo-wide gates — only targeted gates ran: presubmit suite on both arms, tsc --noEmit in packages/cli at head (0 errors). The base control build logs one environmental error in core (@lydell/node-pty declaration resolution from the shared node_modules), absent from the PR's files and from the head typecheck; it did not suppress emit.

Methodology

Environment: CI merge-ref checkout (HEAD 53668c9, HEAD^1 base tip, HEAD^2 verified head), npm ci + npm run build pre-run at head. Base arm: scratch worktree at HEAD^1, packages/cli rebuilt there with unchanged reference-package outputs and nested node_modules symlinked from the head install (lockfile identical; git diff HEAD^1..HEAD over those packages is empty); worktree removed after capture. Harnesses live in harness/ (run.mjs drives both arms' real dist/index.js review presubmit through harness/bin/gh; print-matrix.mjs prints the recorded matrix); raw logs in logs/ (ab-run.log, base-build.log, m3-run.log, per-cell scratch logs consumed during the run). Assertion counts: harness 240 + head suite 110 + base suite 106 + 4 mutation expectations + 1 typecheck = 461.

Flakiness gate log

rounds=5 files=1 skipped=0
file packages/cli/src/commands/review/presubmit.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/review/presubmit.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/commands/review/presubmit.test.ts: PPPPP

verdict: pass
summary: 1 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/commands/review/presubmit.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/review/presubmit.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/review/presubmit.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/review/presubmit.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/review/presubmit.test.ts: P (exit 0)

Evidence images

01-ab-range-overlap-base-vs-head

02-mutation-matrix

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao
wenshao added this pull request to the merge queue Aug 31, 2026
Merged via the queue into QwenLM:main with commit 6828ac7 Aug 31, 2026
81 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants